import isEditable from 'dom-element-is-natively-editable';

const CTRL = 1 << 0;
const META = 1 << 1;
const ALT = 1 << 2;
const SHIFT = 1 << 3;

// Key Events
export const KeyEvents: any = {
  DOWN: 1 << 0,
  PRESS: 1 << 1,
  UP: 1 << 2,
  INPUT: 1 << 3
};
KeyEvents.ALL = KeyEvents.DOWN | KeyEvents.PRESS | KeyEvents.UP | KeyEvents.INPUT;

/**
 * Represents a keystroke, or a single key code with a set of active modifiers.
 *
 * @class Keystroke
 */
export class Keystroke {


  modifiers: any
  ctrlKey: any
  metaKey: any
  altKey: any
  shiftKey: any
  keyCode: any

  /**
   * @param {number} modifiers A bitmask formed by CTRL, META, ALT, and SHIFT.
   * @param {number} keyCode
   */
  constructor(modifiers: any, keyCode: any) {
    this.modifiers = modifiers;
    this.ctrlKey = !!(modifiers & CTRL);
    this.metaKey = !!(modifiers & META);
    this.altKey = !!(modifiers & ALT);
    this.shiftKey = !!(modifiers & SHIFT);
    this.keyCode = keyCode;
  }

  /**
   * Gets the bitmask value for the "control" modifier.
   *
   * @type {number}
   */
  static CTRL = CTRL;

  /**
   * Gets the bitmask value for the "meta" modifier.
   *
   * @return {number}
   */
  static META = META;

  /**
   * Gets the bitmask value for the "alt" modifier.
   *
   * @return {number}
   */
  static ALT = ALT;

  /**
   * Gets the bitmask value for the "shift" modifier.
   *
   * @return {number}
   */
  static SHIFT = SHIFT;
}

/**
 * Simulates a keyboard with a particular key-to-character and key-to-action
 * mapping. Use `US_ENGLISH` to get a pre-configured keyboard.
 */
export class Keyboard {


  static US_ENGLISH: any

  _charCodeKeyCodeMap: any
  _actionKeyCodeMap: any

  constructor(charCodeKeyCodeMap: any, actionKeyCodeMap: any) {
    this._charCodeKeyCodeMap = charCodeKeyCodeMap;
    this._actionKeyCodeMap = actionKeyCodeMap;
  }


  charCodeForKeystroke(keystroke: any) {
    let map = this._charCodeKeyCodeMap;
    for (let charCode in map) {
      if (Object.prototype.hasOwnProperty.call(map, charCode)) {
        let keystrokeForCharCode = map[charCode];
        if (keystroke.keyCode === keystrokeForCharCode.keyCode &&
          keystroke.modifiers === keystrokeForCharCode.modifiers) {
          return parseInt(charCode, 10);
        }
      }
    }
    return null;
  }


  createEventFromKeystroke(type: any, keystroke: any, target: any) {
    const document = target.ownerDocument;
    const window = document.defaultView;
    const Event = window.Event;

    let event;

    try {
      event = new Event(type);
    } catch (e) {
      event = document.createEvent('UIEvents');
    }

    event.initEvent(type, true, true);

    switch (type) {
      case 'textInput':
        event.data = String.fromCharCode(this.charCodeForKeystroke(keystroke));
        break;

      case 'keydown':
      case 'keypress':
      case 'keyup':
        event.shiftKey = keystroke.shiftKey;
        event.altKey = keystroke.altKey;
        event.metaKey = keystroke.metaKey;
        event.ctrlKey = keystroke.ctrlKey;
        event.keyCode = type === 'keypress' ? this.charCodeForKeystroke(keystroke) : keystroke.keyCode;
        event.charCode = type === 'keypress' ? event.keyCode : 0;
        event.which = event.keyCode;
        break;
    }

    return event;
  }


  dispatchEventsForAction(action: any, target: any) {
    const keystroke = this.keystrokeForAction(action);
    this.dispatchEventsForKeystroke(keystroke, target);
  }


  dispatchEventsForInput(input: any, target: any) {
    let currentModifierState = 0;
    for (let i = 0, length = input.length; i < length; i++) {
      const keystroke = this.keystrokeForCharCode(input.charCodeAt(i));
      if (!keystroke) continue

      this.dispatchModifierStateTransition(target, currentModifierState, keystroke.modifiers);
      this.dispatchEventsForKeystroke(keystroke, target, false);
      currentModifierState = keystroke.modifiers;
    }
    this.dispatchModifierStateTransition(target, currentModifierState, 0);
  }


  dispatchEventsForKeystroke(keystroke: any, target: any, transitionModifiers = true, events = KeyEvents.ALL) {
    if (!keystroke) return

    if (transitionModifiers) {
      this.dispatchModifierStateTransition(target, 0, keystroke.modifiers, events);
    }

    let keydownEvent;
    if (events & KeyEvents.DOWN) {
      keydownEvent = this.createEventFromKeystroke('keydown', keystroke, target);
    }

    if (keydownEvent && target.dispatchEvent(keydownEvent) && this.targetCanReceiveTextInput(target)) {
      let keypressEvent;
      if (events & KeyEvents.PRESS) {
        keypressEvent = this.createEventFromKeystroke('keypress', keystroke, target);
      }
      if (keypressEvent && keypressEvent.charCode && target.dispatchEvent(keypressEvent)) {
        if (events & KeyEvents.INPUT) {
          const textinputEvent = this.createEventFromKeystroke('textInput', keystroke, target);
          target.dispatchEvent(textinputEvent);

          const inputEvent = this.createEventFromKeystroke('input', keystroke, target);
          target.dispatchEvent(inputEvent);
        }
      }
    }

    if (events & KeyEvents.UP) {
      const keyupEvent = this.createEventFromKeystroke('keyup', keystroke, target);
      target.dispatchEvent(keyupEvent);
    }

    if (transitionModifiers) {
      this.dispatchModifierStateTransition(target, keystroke.modifiers, 0);
    }
  }


  dispatchModifierStateTransition(target: any, fromModifierState: any, toModifierState: any, events = KeyEvents.ALL) {
    let currentModifierState = fromModifierState;
    let didHaveMeta = (fromModifierState & META) === META;
    let willHaveMeta = (toModifierState & META) === META;
    let didHaveCtrl = (fromModifierState & CTRL) === CTRL;
    let willHaveCtrl = (toModifierState & CTRL) === CTRL;
    let didHaveShift = (fromModifierState & SHIFT) === SHIFT;
    let willHaveShift = (toModifierState & SHIFT) === SHIFT;
    let didHaveAlt = (fromModifierState & ALT) === ALT;
    let willHaveAlt = (toModifierState & ALT) === ALT;

    const includeKeyUp = events & KeyEvents.UP;
    const includeKeyDown = events & KeyEvents.DOWN;

    if (includeKeyUp && didHaveMeta === true && willHaveMeta === false) {
      // Release the meta key.
      currentModifierState &= ~META;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keyup',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.META),
          target
        )
      );
    }

    if (includeKeyUp && didHaveCtrl === true && willHaveCtrl === false) {
      // Release the ctrl key.
      currentModifierState &= ~CTRL;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keyup',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.CTRL),
          target
        )
      );
    }

    if (includeKeyUp && didHaveShift === true && willHaveShift === false) {
      // Release the shift key.
      currentModifierState &= ~SHIFT;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keyup',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.SHIFT),
          target
        )
      );
    }

    if (includeKeyUp && didHaveAlt === true && willHaveAlt === false) {
      // Release the alt key.
      currentModifierState &= ~ALT;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keyup',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.ALT),
          target
        )
      );
    }

    if (includeKeyDown && didHaveMeta === false && willHaveMeta === true) {
      // Press the meta key.
      currentModifierState |= META;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keydown',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.META),
          target
        )
      );
    }

    if (includeKeyDown && didHaveCtrl === false && willHaveCtrl === true) {
      // Press the ctrl key.
      currentModifierState |= CTRL;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keydown',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.CTRL),
          target
        )
      );
    }

    if (includeKeyDown && didHaveShift === false && willHaveShift === true) {
      // Press the shift key.
      currentModifierState |= SHIFT;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keydown',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.SHIFT),
          target
        )
      );
    }

    if (includeKeyDown && didHaveAlt === false && willHaveAlt === true) {
      // Press the alt key.
      currentModifierState |= ALT;
      target.dispatchEvent(
        this.createEventFromKeystroke(
          'keydown',
          new Keystroke(currentModifierState, this._actionKeyCodeMap.ALT),
          target
        )
      );
    }

    if (currentModifierState !== toModifierState) {
      throw new Error(
        `internal error, expected modifier state: ${toModifierState}` +
        `, got: ${currentModifierState}`
      );
    }
  }

  keystrokeForAction(action: any) {
    let keyCode = null;
    let modifiers = 0;

    // Note: when it comes to a single character as '+',
    // should not take it as a key combiniation (no action.split)
    let parts = action.length === 1 ? [action] : action.split('+');
    let lastPart = parts.pop();

    parts.forEach((part) => {
      switch (part.toUpperCase()) {
        case 'CTRL':
          modifiers |= CTRL;
          break;
        case 'META':
          modifiers |= META;
          break;
        case 'ALT':
          modifiers |= ALT;
          break;
        case 'SHIFT':
          modifiers |= SHIFT;
          break;
        default:
          console.error('parts', parts)
          throw new Error(`in "${action}", invalid modifier: ${part}`);
      }
    });

    if (lastPart.toUpperCase() in this._actionKeyCodeMap) {
      keyCode = this._actionKeyCodeMap[lastPart.toUpperCase()];
    } else if (lastPart.length === 1) {
      let lastPartKeystroke = this.keystrokeForCharCode(lastPart.charCodeAt(0));
      if (!lastPartKeystroke) return null

      modifiers |= lastPartKeystroke.modifiers;
      keyCode = lastPartKeystroke.keyCode;
    } else {
      throw new Error(`in "${action}", invalid action: ${lastPart}`);
    }

    return new Keystroke(modifiers, keyCode);
  }


  keystrokeForCharCode(charCode: any) {
    return this._charCodeKeyCodeMap[charCode] || null;
  }


  targetCanReceiveTextInput(target: any) {
    if (!target) {
      return false;
    }

    return isEditable(target);
  }
}

const US_ENGLISH_CHARCODE_KEYCODE_MAP = {
  32: new Keystroke(0, 32), // <space>
  33: new Keystroke(SHIFT, 49), // !
  34: new Keystroke(SHIFT, 222), // "
  35: new Keystroke(SHIFT, 51), // #
  36: new Keystroke(SHIFT, 52), // $
  37: new Keystroke(SHIFT, 53), // %
  38: new Keystroke(SHIFT, 55), // &
  39: new Keystroke(0, 222), // '
  40: new Keystroke(SHIFT, 57), // (
  41: new Keystroke(SHIFT, 48), // )
  42: new Keystroke(SHIFT, 56), // *
  43: new Keystroke(SHIFT, 187), // +
  44: new Keystroke(0, 188), // ,
  45: new Keystroke(0, 189), // -
  46: new Keystroke(0, 190), // .
  47: new Keystroke(0, 191), // /
  48: new Keystroke(0, 48), // 0
  49: new Keystroke(0, 49), // 1
  50: new Keystroke(0, 50), // 2
  51: new Keystroke(0, 51), // 3
  52: new Keystroke(0, 52), // 4
  53: new Keystroke(0, 53), // 5
  54: new Keystroke(0, 54), // 6
  55: new Keystroke(0, 55), // 7
  56: new Keystroke(0, 56), // 8
  57: new Keystroke(0, 57), // 9
  58: new Keystroke(SHIFT, 186), // :
  59: new Keystroke(0, 186), // ;
  60: new Keystroke(SHIFT, 188), // <
  61: new Keystroke(0, 187), // =
  62: new Keystroke(SHIFT, 190), // >
  63: new Keystroke(SHIFT, 191), // ?
  64: new Keystroke(SHIFT, 50), // @
  65: new Keystroke(SHIFT, 65), // A
  66: new Keystroke(SHIFT, 66), // B
  67: new Keystroke(SHIFT, 67), // C
  68: new Keystroke(SHIFT, 68), // D
  69: new Keystroke(SHIFT, 69), // E
  70: new Keystroke(SHIFT, 70), // F
  71: new Keystroke(SHIFT, 71), // G
  72: new Keystroke(SHIFT, 72), // H
  73: new Keystroke(SHIFT, 73), // I
  74: new Keystroke(SHIFT, 74), // J
  75: new Keystroke(SHIFT, 75), // K
  76: new Keystroke(SHIFT, 76), // L
  77: new Keystroke(SHIFT, 77), // M
  78: new Keystroke(SHIFT, 78), // N
  79: new Keystroke(SHIFT, 79), // O
  80: new Keystroke(SHIFT, 80), // P
  81: new Keystroke(SHIFT, 81), // Q
  82: new Keystroke(SHIFT, 82), // R
  83: new Keystroke(SHIFT, 83), // S
  84: new Keystroke(SHIFT, 84), // T
  85: new Keystroke(SHIFT, 85), // U
  86: new Keystroke(SHIFT, 86), // V
  87: new Keystroke(SHIFT, 87), // W
  88: new Keystroke(SHIFT, 88), // X
  89: new Keystroke(SHIFT, 89), // Y
  90: new Keystroke(SHIFT, 90), // Z
  91: new Keystroke(0, 219), // [
  92: new Keystroke(0, 220), // \
  93: new Keystroke(0, 221), // ]
  94: new Keystroke(SHIFT, 54), // ^
  95: new Keystroke(SHIFT, 189), // _
  96: new Keystroke(0, 192), // `
  97: new Keystroke(0, 65), // a
  98: new Keystroke(0, 66), // b
  99: new Keystroke(0, 67), // c
  100: new Keystroke(0, 68), // d
  101: new Keystroke(0, 69), // e
  102: new Keystroke(0, 70), // f
  103: new Keystroke(0, 71), // g
  104: new Keystroke(0, 72), // h
  105: new Keystroke(0, 73), // i
  106: new Keystroke(0, 74), // j
  107: new Keystroke(0, 75), // k
  108: new Keystroke(0, 76), // l
  109: new Keystroke(0, 77), // m
  110: new Keystroke(0, 78), // n
  111: new Keystroke(0, 79), // o
  112: new Keystroke(0, 80), // p
  113: new Keystroke(0, 81), // q
  114: new Keystroke(0, 82), // r
  115: new Keystroke(0, 83), // s
  116: new Keystroke(0, 84), // t
  117: new Keystroke(0, 85), // u
  118: new Keystroke(0, 86), // v
  119: new Keystroke(0, 87), // w
  120: new Keystroke(0, 88), // x
  121: new Keystroke(0, 89), // y
  122: new Keystroke(0, 90), // z
  123: new Keystroke(SHIFT, 219), // {
  124: new Keystroke(SHIFT, 220), // |
  125: new Keystroke(SHIFT, 221), // }
  126: new Keystroke(SHIFT, 192) // ~
};

const US_ENGLISH_ACTION_KEYCODE_MAP = {
  BACKSPACE: 8,
  TAB: 9,
  ENTER: 13,
  SHIFT: 16,
  CTRL: 17,
  ALT: 18,
  PAUSE: 19,
  CAPSLOCK: 20,
  ESCAPE: 27,
  PAGEUP: 33,
  PAGEDOWN: 34,
  END: 35,
  HOME: 36,
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40,
  INSERT: 45,
  DELETE: 46,
  META: 91,
  F1: 112,
  F2: 113,
  F3: 114,
  F4: 115,
  F5: 116,
  F6: 117,
  F7: 118,
  F8: 119,
  F9: 120,
  F10: 121,
  F11: 122,
  F12: 123
};

/**
 * Gets a keyboard instance configured as a U.S. English keyboard would be.
 *
 * @return {Keyboard}
 */
Keyboard.US_ENGLISH = new Keyboard(
  US_ENGLISH_CHARCODE_KEYCODE_MAP,
  US_ENGLISH_ACTION_KEYCODE_MAP
);
